package com.zhss.dfs.namenode.server;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

/**
 * 负责管理edits log日志的核心组件
 * @author zhonghuashishan
 *
 */
public class FSEditlog {

	/**
	 *单例，每个系统中只需要一个日志管理核心组件
	 */
	private static volatile FSEditlog instance;
	public static FSEditlog getInstance(){
		if(instance==null){
			synchronized (FSEditlog.class){
				if(instance==null){
					instance=new FSEditlog();
				}
			}
		}
		return instance;
	}
	private FSEditlog(){}

	/**
	 * 每一笔日志记录都有唯一的一个递增的id
	 */
	private Long txIdSeq=0L;
	/**
	 * 双缓存机制
	 */
	private DoubleBuffer doubleBuffer=new DoubleBuffer();

    /**
     * 在同步到磁盘中的最大的一个txid
     */
    private volatile Long syncMaxTxid = 0L;
    /**
     *每个线程自己本地的txId副本
     */
	ThreadLocal<Long> localTxid=new ThreadLocal<Long>();

    /**
     *看是否有线程已经在等待将下一批edit log刷盘
     */
	private volatile Boolean isWaitSync=false;
    /**
     * 是否有线程正在刷盘
     */
	private volatile Boolean isSyncRunning=false;
	/**
	 * 记录edits log日志
	 * @param content
	 */
	public void logEdit(String content) {
		synchronized (this){
			//每次来txIdSqe都会进行自增，保证id唯一
			txIdSeq++;
            localTxid.set(txIdSeq);
			//构建一条日志记录
			EditLog editLog = new EditLog(txIdSeq, content);
			//将日志写入缓存中
			doubleBuffer.writeEditLog(editLog);
		}
        logSync();
	}

    /**
     * 将内存中的数据刷入磁盘
     * 这里尝试使用某一个线程将syncBuffer中的所有数据进行刷盘
     */
    private void logSync() {
        //再次尝试加锁
        synchronized (this){
            if(isSyncRunning){
                //有线程正在执行刷盘操作

                //获取已经刷盘的最大syncMaxTxid 与本线程对应的最大txId比较
                //如果txId<syncTxId说明有线程将本线程对应的最大edit log已经刷盘，直接返回
                if(localTxid.get()<=syncMaxTxid){
                    return;
                }

                //如果已经有线程等待刷盘，那么就直接返回
                if(isWaitSync){
                    return;
                }

                //只有一个线程在刷盘，还没有等待再次刷盘的线程
                //此时应该等待刷盘结束，并且释放占用的锁资源，然其他线程写入edit log
                isWaitSync=true;
                while (isSyncRunning){
                    try {
                        //等待2秒，自动唤醒
                        wait(2000);
                    }catch (Exception e){
                        System.out.printf(e.toString());
                    }
                }
                //被唤醒之后，等待刷盘线程变成可以刷盘线程
                //等待结束
                isWaitSync=false;
            }
            // 设置当前正在同步到磁盘的标志位
            isSyncRunning = true;
            //交换两个缓存区，为刷盘做准备
            doubleBuffer.setReadyToSync();
            //在刷盘之前保存一下，此次刷盘的最大txId；
            syncMaxTxid=doubleBuffer.getMaxTxId();
        }
        //将syncBuffer中的数据刷盘
        doubleBuffer.flush();
        //刷完盘之后,将等待下一次刷盘的线程唤醒
        synchronized (this){
            //完盘之后，将正在刷盘的标志位复位，以唤醒下一次刷盘的线程
            isSyncRunning=false;
            //唤醒所有等待下一次刷盘的线程，实际上只有一个
            notifyAll();
        }
    }

    /**
	 * 一条日志记录
	 */
	private class EditLog{
		/**
		 * 每条日志的唯一id
		 */
		Long txIdSeq;
		/**
		 * 日志记录的内容
		 */
		String content;

		public Long getTxIdSeq() {
			return txIdSeq;
		}

		public void setTxIdSeq(Long txIdSeq) {
			this.txIdSeq = txIdSeq;
		}

		public String getContent() {
			return content;
		}

		public void setContent(String content) {
			this.content = content;
		}

		public EditLog(Long txIdSeq, String content) {
			this.txIdSeq = txIdSeq;
			this.content = content;
		}

		@Override
		public String toString() {
			return "EditLog{" +
					"txIdSeq=" + txIdSeq +
					", content='" + content + '\'' +
					'}';
		}
	}
	private class DoubleBuffer{
		/**
		 * 主要用来承载线程写入edit log
		 */
		List<EditLog> currentBuffer=new LinkedList<EditLog>();

		/**
		 * 这块内容用来将内存中的数据刷盘
		 */
		List<EditLog> syncBuffer=new LinkedList<EditLog>();

		/**
		 * 将edit log写入内存缓冲中
		 * @param editLog
		 */
		public void writeEditLog(EditLog editLog){
			currentBuffer.add(editLog);
		}

		/**
		 * 交换currentBuffer和syncBuffer，准备将内存中的数据写入磁盘
		 */
		public void setReadyToSync(){
			List<EditLog> temp=currentBuffer;
			currentBuffer=syncBuffer;
			syncBuffer=temp;
		}

		/**
		 * 将内存中的数据刷盘
		 */
		public void flush(){
			for (EditLog editLog : syncBuffer) {
				System.out.println("数据刷盘，"+editLog);
			}
		}

        /**
         * 获取currentBuffer中edit log的数量
         * 到达一定数量之后就调用setReadyToSync,交换currentBuffer和syncBuffer
         * 之后再调用syncEditLog
         * @return
         */
        public Integer getCurrentBufferSize() {
            return this.currentBuffer.size();
        }

        /**
         * 获取syncBuffer中最大的txId;
         * @return
         */
        public Long getMaxTxId(){
            EditLog last = (EditLog) ((LinkedList) syncBuffer).getLast();
            return last.getTxIdSeq();
        }
    }
}
